﻿/*************************************************************************
 * Created by OhZee Creative, LLC
 * 2013
 * This code is licensed under Microsoft Public License (Ms-PL)
 * http://opensource.org/licenses/ms-pl
 * http://ozcontrols.codeplex.com/
 *************************************************************************/

namespace OhZeeCreative.UI.Controls
{
    using System;
    using System.ComponentModel;
    using System.Linq;
    using OhZeeCreative.Helpers;

#if SILVERLIGHT
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Shapes;
    using System.Windows.Media.Animation;
    using System.Windows.Media;
    using System.Windows.Markup;
#endif
#if NETFX_CORE
    using Windows.Foundation;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Media;
    using Windows.UI.Xaml.Media.Animation;
    using Windows.UI.Xaml.Shapes;
#endif

    /// <summary>
    /// RadialControl is a very flexible 'arc' control whose start and end angles, radii, thickness can be specified. You can create simple arcs and circles, or complicated curved shapes. You can get very creative with the RadialControl. By adjusting a few properties, you can create spirals by or even regular polygons.
    /// </summary>
    [EditorBrowsableAttribute(EditorBrowsableState.Always)]
    [TemplatePart(Name = RadialControl.TEMPLATE_PART_arc, Type = typeof(Polygon))]
    [TemplatePart(Name = RadialControl.TEMPLATE_PART_chh, Type = typeof(Line))]
    [TemplatePart(Name = RadialControl.TEMPLATE_PART_chv, Type = typeof(Line))]
	public sealed class RadialControl : Control
	{
        /// <summary>
        /// Raised whenever the RadialControl is updated.
        /// </summary>
		public event EventHandler Updated;
        
        /// <summary>
        /// The name of the Polygon used in the control template
        /// </summary>
        public const string TEMPLATE_PART_arc = "arc";

        /// <summary>
        /// The name of the horizontal line in the control template that's used to display the crosshairs (optional)
        /// </summary>
        public const string TEMPLATE_PART_chh = "crossHairHorizontal";

        /// <summary>
        /// The name of the vertical line in the control template that's used to display the crosshairs (optional)
        /// </summary>
        public const string TEMPLATE_PART_chv = "crossHairVertical";

		private Polygon arc;
		private Line crossHairHorizontal;
		private Line crossHairVertical;

		Point[] outerPoints;
		Point[] innerPoints;

		// If AngleBegin > AngleEnd, the arc will be clockwise. Otherwise, it will be anti-clockwise. Negative values are acceptable.
		public static readonly DependencyProperty AngleBeginProperty = DependencyProperty.Register("AngleBegin", typeof(double), typeof(RadialControl), new PropertyMetadata(180.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty AngleEndProperty = DependencyProperty.Register("AngleEnd", typeof(double), typeof(RadialControl), new PropertyMetadata(90.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty RadiusBeginProperty = DependencyProperty.Register("RadiusBegin", typeof(double), typeof(RadialControl), new PropertyMetadata(200.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty RadiusEndProperty = DependencyProperty.Register("RadiusEnd", typeof(double), typeof(RadialControl), new PropertyMetadata(200.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty ThicknessBeginProperty = DependencyProperty.Register("ThicknessBegin", typeof(double), typeof(RadialControl), new PropertyMetadata(10.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty ThicknessEndProperty = DependencyProperty.Register("ThicknessEnd", typeof(double), typeof(RadialControl), new PropertyMetadata(10.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty EasingProperty = DependencyProperty.Register("Easing", typeof(EasingFunctionBase), typeof(RadialControl), new PropertyMetadata(null, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty PointsProperty = DependencyProperty.Register("Points", typeof(int), typeof(RadialControl), new PropertyMetadata(60, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty ShowCrosshairsAtOriginProperty = DependencyProperty.Register("ShowCrosshairsAtOrigin", typeof(bool), typeof(RadialControl), new PropertyMetadata(true));
		public static readonly DependencyProperty ShapeWidthProperty = DependencyProperty.Register("ShapeWidth", typeof(double), typeof(RadialControl), new PropertyMetadata(0.0));
		public static readonly DependencyProperty ShapeHeightProperty = DependencyProperty.Register("ShapeHeight", typeof(double), typeof(RadialControl), new PropertyMetadata(0.0));
		public static readonly DependencyProperty OriginXProperty = DependencyProperty.Register("OriginX", typeof(double), typeof(RadialControl), new PropertyMetadata(0.0));
		public static readonly DependencyProperty OriginYProperty = DependencyProperty.Register("OriginY", typeof(double), typeof(RadialControl), new PropertyMetadata(0.0));
        public static readonly DependencyProperty CenterOriginProperty = DependencyProperty.Register("CenterOrigin", typeof(bool), typeof(RadialControl), new PropertyMetadata(true, GenericRenderInvalidatingPropertyChanged));

		// Shape like properties
		public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register("StrokeThickness", typeof(double), typeof(RadialControl), new PropertyMetadata(1.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register("Stroke", typeof(Brush), typeof(RadialControl), new PropertyMetadata(null));
				
		private static void GenericRenderInvalidatingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			RadialControl radialControl = (RadialControl)d;
			radialControl.Update();
		}

		#region Properties for dependency properties

        /// <summary>
        /// Gets or sets the length of the radius at the start angle, in pixels.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double RadiusBegin
		{
			get { return (double)this.GetValue(RadiusBeginProperty); }
			set { this.SetValue(RadiusBeginProperty, value); }
		}

        /// <summary>
        /// Gets or sets the length of the radius at the end angle, in pixels.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double RadiusEnd
		{
			get { return (double)this.GetValue(RadiusEndProperty); }
			set { this.SetValue(RadiusEndProperty, value); }
		}

        /// <summary>
        /// Gets or sets the starting angle in degrees according to Cartesian coordinates.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double AngleBegin
		{
			get { return (double)this.GetValue(AngleBeginProperty); }
			set { this.SetValue(AngleBeginProperty, value); }
		}

        /// <summary>
        /// Gets or sets the ending angle in degrees according to Cartesian coordinates.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double AngleEnd
		{
			get { return (double)this.GetValue(AngleEndProperty); }
			set { this.SetValue(AngleEndProperty, value); }
		}

        /// <summary>
        /// Gets or sets the thickness at the start angle, in pixels.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double ThicknessBegin
		{
			get { return (double)this.GetValue(ThicknessBeginProperty); }
			set { this.SetValue(ThicknessBeginProperty, value); }
		}

        /// <summary>
        /// Gets or sets the thickness at the end angle, in pixels.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double ThicknessEnd
		{
			get { return (double)this.GetValue(ThicknessEndProperty); }
			set { this.SetValue(ThicknessEndProperty, value); }
		}

        /// <summary>
        /// Gets or sets the easing that determines how intermediate values of radius and angle are calculated between start and end.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public EasingFunctionBase Easing
		{
			get { return (EasingFunctionBase)this.GetValue(EasingProperty); }
			set { this.SetValue(EasingProperty, value); }
		}

        /// <summary>
        /// Gets or sets the number of points used to draw the arc on one side. The bigger the number, the smoother the arc.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public int Points
		{
			get { return (int)this.GetValue(PointsProperty); }
			set { this.SetValue(PointsProperty, value); }
		}

        /// <summary>
        /// Gets or sets the thickness of the stroke, in pixels.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double StrokeThickness
		{
			get { return (double)this.GetValue(StrokeThicknessProperty); }
			set { this.SetValue(StrokeThicknessProperty, value); }
		}

        /// <summary>
        /// Gets or sets the brush used for the stroke.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public Brush Stroke
		{
			get { return (Brush)this.GetValue(StrokeProperty); }
			set { this.SetValue(StrokeProperty, value); }
		}

        /// <summary>
        /// Toggles the visibility of crosshairs at the origin.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public bool ShowCrosshairsAtOrigin
		{
			get { return (bool)this.GetValue(ShowCrosshairsAtOriginProperty); }
			set { this.SetValue(ShowCrosshairsAtOriginProperty, value); }
		}

        /// <summary>
        /// Gets the X origin relative to the left of the shape area.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
		public double OriginX
		{
            get { return (double)this.GetValue(OriginXProperty); }
            private set { this.SetValue(OriginXProperty, value); }
		}

        /// <summary>
        /// Gets the Y origin relative to the top of the shape area.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
		public double OriginY
		{
            get { return (double)this.GetValue(OriginYProperty); }
            private set { this.SetValue(OriginYProperty, value); }
		}


        /// <summary>
        /// Determines whether the origin should be centered within the width and height of the control.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
        public bool CenterOrigin
        {
            get { return (bool)this.GetValue(CenterOriginProperty); }
            set { this.SetValue(CenterOriginProperty, value); }
        }

        /// <summary>
        /// Gets the internal width of the shape.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
		public double ShapeWidth
		{
			get { return (double)GetValue(ShapeWidthProperty); }
			private set { SetValue(ShapeWidthProperty, value); }
		}

        /// <summary>
        /// Gets the internal height of the shape.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
		public double ShapeHeight
		{
			get { return (double)GetValue(ShapeHeightProperty); }
			private set { SetValue(ShapeHeightProperty, value); }
		}

		#endregion

		public RadialControl()
		{
			this.DefaultStyleKey = typeof(RadialControl);

			this.SizeChanged += (s, e) =>
			{
				this.Update();
			};

            // TODO: BUGBUG: was this ever intended
#if SILVERLIGHTa 
            this.Template = (ControlTemplate) XamlReader.Load(
                @"<ControlTemplate 
                    xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
                    xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
                    xmlns:ozc=""clr-namespace:OhZeeCreative.WindowsPhone;assembly=OhZeeCreative.WindowsPhone.Utilities""
                    xmlns:radials=""clr-namespace:OhZeeCreative.Silverlight.UI.Radials;assembly=OhZeeCreative.Silverlight.UI.Radials""
                    TargetType=""radials:RadialControl"">
                    <Border Background=""Blue"" BorderBrush=""Green"" BorderThickness=""3"">
                        <Grid>
                            <Polygon x:Name=""arc"" Fill=""Red"" Stroke=""Cyan"" StrokeThickness=""1"" />
                            <Line x:Name=""crossHairHorizontal"" Visibility=""{Binding ShowCrosshairsAtOrigin, Converter={StaticResource btvConverter}, RelativeSource={RelativeSource TemplatedParent}}"" Stroke=""#80808080""  StrokeThickness=""1.0""/>
                            <Line x:Name=""crossHairVertical""  Visibility=""{Binding ShowCrosshairsAtOrigin, Converter={StaticResource btvConverter}, RelativeSource={RelativeSource TemplatedParent}}""  Stroke=""#80808080""  StrokeThickness=""1.0""/>
                        </Grid>
                    </Border>
                </ControlTemplate>");
#endif
		}

#if SILVERLIGHT
        public override void OnApplyTemplate()
#endif
#if NETFX_CORE
		protected override void OnApplyTemplate()
#endif
		{
			this.arc = this.GetTemplateChild(RadialControl.TEMPLATE_PART_arc) as Polygon;
			this.crossHairHorizontal = this.GetTemplateChild(RadialControl.TEMPLATE_PART_chh) as Line;
			if (this.crossHairHorizontal != null)
			{
				this.crossHairHorizontal.DataContext = this;
			}
            this.crossHairVertical = this.GetTemplateChild(RadialControl.TEMPLATE_PART_chv) as Line;
			if (this.crossHairVertical != null)
			{
				this.crossHairVertical.DataContext = this;
			}

            this.Update();
		}

		private void OnUpdated()
		{
			if (this.Updated != null)
			{
				this.Updated(this, EventArgs.Empty);
			}
		}

		private Point[] GetOuterRadialControlCoordinates()
		{
			Point[] points = RadialHelper.GetRadialCoordinatesForDegrees(
				this.AngleBegin, 
				this.AngleEnd,
				this.RadiusBegin + this.ThicknessBegin,
				this.RadiusEnd + this.ThicknessEnd,
				this.Points,
				this.Easing);

			return points;
		}

		private Point[] GetInnerRadialControlCoordinates()
		{
			Point[] points = RadialHelper.GetRadialCoordinatesForDegrees(
				this.AngleBegin, 
				this.AngleEnd, 
				this.RadiusBegin, 
				this.RadiusEnd,
				this.Points, 
				this.Easing);

			return points;
		}

		private void AddPoints()
		{
			if (this.arc == null)
			{
				return;
			}

#if SILVERLIGHT
            Point origin = new Point(this.OriginX, this.OriginY);
#endif

#if NETFX_CORE
			Point origin;
			origin.X = this.OriginX;
			origin.Y = this.OriginY;
#endif

			this.arc.Points.Clear();

			Point[] outerPoints = this.GetOuterRadialControlCoordinates();

			foreach (Point point in outerPoints.Select<Point, Point>(point => LayoutHelper.GetOffsetPoint(point, origin)))
			{
				this.arc.Points.Add(point);
			}
						
			Point[] innerPoints = this.GetInnerRadialControlCoordinates();

			foreach (Point point in innerPoints.Select<Point, Point>(point => LayoutHelper.GetOffsetPoint(point, origin)).Reverse())
			{
				this.arc.Points.Add(point);
			}
		}

		private void UpdateOrigin()
		{
			const double width = 10;
			const double height = 10;

			if (this.crossHairHorizontal != null)
			{
				this.crossHairHorizontal.X1 = this.OriginX - width.Halved();
				this.crossHairHorizontal.Y1 = this.OriginY;
				this.crossHairHorizontal.X2 = this.OriginX + width.Halved();
				this.crossHairHorizontal.Y2 = this.OriginY;
			}

			if (this.crossHairVertical != null)
			{
				this.crossHairVertical.X1 = this.OriginX;
				this.crossHairVertical.Y1 = this.OriginY - height.Halved();
				this.crossHairVertical.X2 = this.OriginX;
				this.crossHairVertical.Y2 = this.OriginY + height.Halved();
			}
		}

		private void Update()
		{
			this.outerPoints = this.GetOuterRadialControlCoordinates();
			this.innerPoints = this.GetInnerRadialControlCoordinates();

			double minX = LayoutHelper.MinimumX(this.outerPoints, this.innerPoints);
			double maxX = LayoutHelper.MaximumX(this.outerPoints, this.innerPoints);
			double width = maxX - minX + this.StrokeThickness;	// outer stroke = half stroke. Calculate for two outer strokes left and right

			double minY = LayoutHelper.MinimumY(this.outerPoints, this.innerPoints);
			double maxY = LayoutHelper.MaximumY(this.outerPoints, this.innerPoints);
			double height = maxY - minY + this.StrokeThickness;	// outer stroke = half stroke. Calculate for two outer strokes top and bottom

			this.ShapeWidth = width;
			this.ShapeHeight = height;

            this.OriginX = this.CenterOrigin ? this.ActualWidth.Halved() : -minX + this.StrokeThickness.Halved();
			this.OriginY = this.CenterOrigin ? this.ActualHeight.Halved() : -minY + this.StrokeThickness.Halved();

			this.AddPoints();
			this.UpdateOrigin();

			this.OnUpdated();
		}
	}
}
